﻿using System;
using System.Collections;
using System.Text;
using System.Threading;

using Mono.Zeroconf;

namespace passitumi
{
	/**
	 * This class is used to browse for peers which are registered under
	 * the same service (or compatible serivce, like Giver eventually).
	 */
	public class ServiceLocator
	{	
		// The singleton instance of the locator
		private static ServiceLocator instance;

		// The actual browser to locate services
		private ServiceBrowser browser;

		// A boolean representing whether or not the service locator is running and whether or not we are actively searching for contacts
		private bool searching = false;

		// The service information
		private string regType = "_passitumi._tcp";
		private string broadcastDomain = "local";

		// This arraylist is used to hold all the network contacts connected (peers)
		private ArrayList networkContactList = new ArrayList();

		// The event handler which is used to receive and handle events from the ServiceLocator
		public delegate void ServiceLocatorEventHandler(object o, ServiceEventArgs args);
		public event ServiceLocatorEventHandler OnServiceLocatorEvent;

		/**
		 * Singleton method to retrieve the ONLY instance of the
		 * ServiceLocator object.
		 */
		public static ServiceLocator getInstance()
		{
			if (ServiceLocator.instance == null)
				ServiceLocator.instance = new ServiceLocator();

			return ServiceLocator.instance;
		}

		/**
		 * Default constructor
		 */
		private ServiceLocator()
		{
			this.browser = new ServiceBrowser();
			browser.ServiceAdded += OnServiceAdded;
			browser.ServiceRemoved += OnServiceRemoved;
		}

		/**
		 * Start looking for network contacts.
		 */
		public void start()
		{
			Console.WriteLine("ServiceLocator: Looking for network services...");
			this.searching = true;
			// Browse for peers that are operating on the used service
			/*
			 * Currently we have to specify that the application is searching on address protocol IPv4 because
			 * of a bug in Mono.Zeroconf (https://bugzilla.novell.com/show_bug.cgi?id=430732).
			 */
			this.browser.Browse(AddressProtocol.IPv4, this.regType, this.broadcastDomain);
		}

		/**
		 * Stop looking for network contacts.
		 */
		public void stop()
		{
			Console.WriteLine("ServiceLocator: No longer looking for network services");
			this.searching = false;
			this.browser.Dispose();
		}

		/**
		 * This method is used to service the event of when a new
		 * peer is discovered. If the peer is valid to be added (a new
		 * peer to be added) then resolve information about it.
		 */
		private void OnServiceAdded(object o, ServiceBrowseEventArgs args)
		{
			string serviceName = args.Service.Name;
			string registrationType = args.Service.RegType;

			// The registration type should equal that of this application, i.e. _passitumi._tcp
			// There are two variants of this to catch Bonjour and Avahi Provider's format
			if (registrationType.Equals(this.regType + ".") || registrationType.Equals(this.regType))
			{
				// Ignore services that originated from this machine
				if (!args.Service.Name.Equals(ServiceBroadcaster.getInstance().getLocalServiceName()))
				{
					Console.WriteLine("ServiceLocator: Service Found - Attempting Resolve [{0}]\n\tService Name: {1}", System.Threading.Thread.CurrentThread.ManagedThreadId, serviceName);

					// Makes an event handler for when a service is resolved for information
					args.Service.Resolved += OnServiceResolved;
					args.Service.Resolve();
				}
				else
				{
					Console.WriteLine("ServiceLocator: Local Service Found - Ignoring [{0}]", System.Threading.Thread.CurrentThread.ManagedThreadId);
				}
			}
		}

		/**
		 * This method is used to service the event of when a peer
		 * is going to be resolved. The peer is actually added to
		 * the list of currently connect peers.
		 */
		private void OnServiceResolved(object o, ServiceResolvedEventArgs args)
		{
			lock (this.networkContactList)
			{
				// Establish a resolvable service from the event args
				IResolvableService service = o as IResolvableService;
				ITxtRecord record = service.TxtRecord;

				string serviceName = service.Name;
				string ipAddress = service.HostEntry.AddressList[0].ToString();
				short port = service.Port;
				string displayName = record["displayName"].ValueString;

				// Check for duplicate peers
				PeerContact contact = checkForDuplicates(serviceName);

				if (contact == null)
				{
					// A previously unknown peer has been found
					Console.WriteLine("ServiceLocator: New Service Resolved - Adding [{0}]\n\tService Name: {1}\n\tIP Address: {2}:{3}", System.Threading.Thread.CurrentThread.ManagedThreadId, serviceName, ipAddress, port);

					// Trigger an event to indicate that the new peer is to be added
					OnServiceLocatorEvent(this, new ServiceEventArgs(ServiceEventArgs.PEER_FOUND, displayName));

					// Store the peer information in an internal list
					PeerContact newContact = new PeerContact(serviceName, ipAddress, port);
					this.networkContactList.Add(newContact);

				}
				else
				{
					// Peer exists already, add new IP to list if we don't already have it
					ArrayList ipAddresses = contact.getIpAddress();
					for (int i = 0; i < ipAddresses.Count; i++)
					{
						if (ipAddresses[i].Equals(ipAddress))
						{
							Console.WriteLine("ServiceLocator: Duplicate Service Resolved with Duplicate IP - Ignoring [{0}]\n\tService Name: {1}\n\tIP Address: {2}:{3}", System.Threading.Thread.CurrentThread.ManagedThreadId, serviceName, ipAddress, port);
							return;
						}
					}
					Console.WriteLine("ServiceLocator: Duplicate Service Resolved with New IP - Adding [{0}]\n\tService Name: {1}\n\tIP Address: {2}:{3}", System.Threading.Thread.CurrentThread.ManagedThreadId, serviceName, ipAddress, port);
					contact.addIpAddress(ipAddress);
				}
			}

		}

		/**
		 * This method is used to service the event of when an existing
		 * peer is removed. The first correctly matched peer name is removed.
		 */
		private void OnServiceRemoved(object o, ServiceBrowseEventArgs args)
		{
			// Establish a resolvable service from the event args
			IResolvableService service = args.Service as IResolvableService;
			Console.WriteLine("ServiceLocator: Service Lost [{0}]\n\tService Name: {1}", System.Threading.Thread.CurrentThread.ManagedThreadId, service.Name);

			// Removes the event handler for this event
			service.Resolved -= OnServiceResolved;

			// Trigger an event to indicate that this peer has been removed
			//OnServiceLocatorEvent(this, new ServiceEventArgs(ServiceEventArgs.PEER_REMOVED, service.Name));

			// Loop through the peer list to find a peer that matches in terms of name, and removes it
			for (int i = 0; i < this.networkContactList.Count; i++)
			{
				PeerContact contact = this.networkContactList[i] as PeerContact;

				// Remove the peer if the names match
				if (contact.getServiceName().Equals(args.Service.Name))
				{
					IpPingManager pingManager = new IpPingManager(args.Service.Name, contact.getIpAddress());
					pingManager.OnIpPingTestSetEvent += new IpPingManager.IpPingTestSetEventHandler(pingSetResultHandler);

					break;
				}
			}
		}

		/**
		 * This method is called when an IpPingManager object has fire the
		 * OnIpPingTestSetEvent event.  Basically this is just executed when a set of pings
		 * has completed, and we can handle determine what the pings have verified.
		 */
		private void pingSetResultHandler(object sender, string serviceName, ArrayList validIpAddresses)
		{
			// Check what the result returned
			//Invoke(new NewPeerDetected(addNewPeerButton), new object[] { args.displayName });

			// No IP Addresses on this peer were valid, remove peer from list and from GUI
			if (validIpAddresses.Count == 0)
			{
				Console.WriteLine("ServiceLocator: Service Unreachable [{0}]\n\tService Name: {1}", System.Threading.Thread.CurrentThread.ManagedThreadId, serviceName);
				this.removeContactByServiceName(serviceName);
				OnServiceLocatorEvent(this, new ServiceEventArgs(ServiceEventArgs.PEER_REMOVED, serviceName));
			}
			else
			{
				Console.WriteLine("ServiceLocator: Ping result set has {0} valid ip addresses [{1}]", validIpAddresses.Count, System.Threading.Thread.CurrentThread.ManagedThreadId);

				PeerContact contact = this.getContactByServiceName(serviceName);

				// Update the contact to have the current ip address information
				contact.setIpAddress(validIpAddresses);
			}
		}

		/**
		 * Check the existing list of contacts for duplicate entries.
		 * This event may occur if a service is reachable on multiple networks.
		 */
		private PeerContact checkForDuplicates(string serviceName)
		{
			for (int i = 0; i < this.networkContactList.Count; i++)
			{
				PeerContact contact = this.networkContactList[i] as PeerContact;

				if (contact.getServiceName().Equals(serviceName))
				{
					return contact;
				}
			}

			return null;
		}

		/**
		   * A method to return the arraylist of all the currently connected peers.
		   */
		public ArrayList getNetworkContactList()
		{
			return this.networkContactList;
		}

		/**
		 * Retrieve the peer contact in our internal list by the
		 * service name.
		 */
		public PeerContact getContactByServiceName(string serviceName)
		{
			// Loop through the peer list to find a peer that matches in terms of name, and return it
			for (int i = 0; i < this.networkContactList.Count; i++)
			{
				PeerContact contact = this.networkContactList[i] as PeerContact;

				// Return the peer if the names match
				if (contact.getServiceName().Equals(serviceName))
					return contact;
			}

			return null;
		}

		/**
		 * Remove the peer contact in our internal list by the
		 * service name.
		 */
		public void removeContactByServiceName(string serviceName)
		{
			// Loop through the peer list to find a peer that matches in terms of name, and return it
			for (int i = 0; i < this.networkContactList.Count; i++)
			{
				PeerContact contact = this.networkContactList[i] as PeerContact;

				// Remove the peer if the names match
				if (contact.getServiceName().Equals(serviceName))
					this.networkContactList.RemoveAt(i);
			}
		}
	}
}